package org.obm.mina;

import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.channels.AsynchronousCloseException;

import javax.net.ssl.SSLException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class ProtocolHandler implements Runnable {

	private static Logger logger = LoggerFactory.getLogger(ProtocolHandler.class);

	private static boolean isSocketError(IOException e) {
		return e instanceof SocketException || e instanceof SSLException
				|| e instanceof AsynchronousCloseException;
	}

	private boolean idle;

	private final Object idleLock = new Object();

	private final Object shutdownLock = new Object();

	protected Socket sock;

	private boolean stopping;
	public ProtocolHandler() {
		idle = true;
		stopping = false;
	}
	/**
	 * Authenticates the client.
	 * 
	 * @return true if authenticated, false if not authenticated
	 * @throws IOException
	 *             if an I/O error occurred
	 */
	protected abstract boolean authenticate() throws IOException;
	/**
	 * Closes any input/output streams with the client. May get called multiple
	 * times, even after connection was refused.
	 */
	protected abstract void dropConnection();
	protected boolean getIdle() {
		synchronized (idleLock) {
			return idle;
		}
	}

	private boolean getShuttingDown() {
		synchronized (shutdownLock) {
			return stopping;
		}
	}

	/**
	 * Called when a connection has been idle for too long. Sends
	 * protocol-specific message to client notifying it that the connection is
	 * being dropped due to idle timeout.
	 */
	protected abstract void notifyIdleConnection();

	/**
	 * Reads and processes one command sent by client.
	 * 
	 * @return true if expecting more commands, false if QUIT command was
	 *         received and server disconnected the connection
	 * @throws Exception
	 *             if an error occurred
	 */
	protected abstract boolean processCommand() throws Exception;

	private void processConnection() throws Exception {
		boolean cont = true;
		while (cont && !getShuttingDown()) {
			try {
				cont = processCommand();
				setIdle(true);
			} catch (IOException e) {
				if (isSocketError(e)) {
					cont = false;
					if (getShuttingDown()) {
						// Unless debug level enabled, don't log connection
						// error if error occurs while socket is being closed
						logger.debug("Shutdown in progress", e);
					} else if (logger.isDebugEnabled()) {
						// For other connection errors, only include full stack
						// trace if debug level enabled.
						logger.info("I/O error while processing connection", e);
					} else {
						logger
								.info("I/O error while processing connection: "
										+ e);
					}
				} else {
					throw e;
				}
			}
		}
	}

	public void run() {
		// This asserts any reuse of a ProtocolHandler object. Once we close a
		// connection
		// we also set it to null, and since there is no setConnection, you
		// can't meaningfully
		// reuse an ProtocolHandler object and if you do we give you this ISE.
		if (sock == null)
			throw new IllegalStateException(
					"Connection can not be null when running ProtocolHandler");
		String remoteAddress = sock.getInetAddress().getHostAddress();

		try {
			if (setupConnection(sock)) {
				if (authenticate())
					processConnection();
				else
					logger.info("Authentication failed for client "
							+ remoteAddress);
			} else {
				logger.info("Connection refused for client " + remoteAddress);
			}
		} catch (SocketTimeoutException e) {
			logger.debug("Idle timeout: " + e);
			notifyIdleConnection();
		} catch (OutOfMemoryError e) {
			logger.error("out of memory", e);
		} catch (Error e) {
			logger.warn("Fatal error occurred while handling connection", e);
		} catch (Throwable e) {
			logger.error("Exception occurred while handling connection", e);
		} finally {
			dropConnection();
			try {
				sock.close();
			} catch (IOException ioe) {
				if (logger.isDebugEnabled()) {
					logger.info("I/O error while closing connection", ioe);
				} else {
					logger.info("I/O error while closing connection: " + ioe);
				}
			}
		}

		logger.info("Handler exiting normally");
	}

	protected void setIdle(boolean idle) {
		synchronized (idleLock) {
			this.idle = idle;
		}
	}

	/**
	 * Performs any necessary setup steps upon connection from client.
	 * 
	 * @param connection
	 *            the Socket for the connection
	 * @return true if connection was accepted; false if refused
	 * @throws IOException
	 *             if an I/O error occurred
	 */
	protected abstract boolean setupConnection(Socket connection)
			throws IOException;
}
